In [ ]:
epochs = 10
# We don't use the whole dataset for efficiency purpose, but feel free to increase these numbers
n_train_items = 640
n_test_items = 640

Part X - 对MNIST进行安全的训练和评估

在构建机器学习即服务解决方案(MLaaS)时,公司可能需要请求其他合作伙伴访问数据以训练其模型。在卫生或金融领域,模型和数据都非常关键:模型参数是业务资产,而数据是严格监管的个人数据。

在这种情况下,一种可能的解决方案是对模型和数据都进行加密,并在加密后的值上训练机器学习模型。例如,这保证了公司不会访问患者的病历,并且医疗机构将无法观察他们贡献的模型。存在几种允许对加密数据进行计算的加密方案,其中包括安全多方计算(SMPC),同态加密(FHE / SHE)和功能加密(FE)。我们将在这里集中讨论多方计算(已在教程5中进行了介绍),它由私有加性共享组成,并依赖于加密协议SecureNN和SPDZ。

本教程的确切设置如下:考虑您是服务器,并且您想对$n$个工作机持有的某些数据进行模型训练。服务器机密共享他的模型,并将每个共享发送给工作机。工作机秘密共享他们的数据并在他们之间交换。在我们将要研究的配置中,有2个工作机:alice和bob。交换共享后,他们每个人现在拥有自己的共享,另一工作机的数据共享和模型共享。现在,计算可以开始使用适当的加密协议来私下训练模型。训练模型后,所有共享都可以发送回服务器以对其进行解密。下图对此进行了说明:

为了举例说明这个过程,让我们假设alice和bob都拥有MNIST数据集的一部分,然后训练一个模型来执行数字分类!

作者:

中文版译者:

1. 在MNIST上进行加密训练的demo

导包以及训练配置


In [ ]:
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms

import time

此类描述了训练的所有超参数。 请注意,它们在这里都是公开的。


In [ ]:
class Arguments():
    def __init__(self):
        self.batch_size = 64
        self.test_batch_size = 64
        self.epochs = epochs
        self.lr = 0.02
        self.seed = 1
        self.log_interval = 1 # Log info at each batch
        self.precision_fractional = 3

args = Arguments()

_ = torch.manual_seed(args.seed)

这是PySyft的进口商品。 我们连接到两个名为alicebob的远程工作机,并请求另一个名为crypto_provider的工作机,它提供了我们可能需要的所有加密原语。


In [ ]:
import syft as sy  # import the Pysyft library
hook = sy.TorchHook(torch)  # hook PyTorch to add extra functionalities like Federated and Encrypted Learning

# simulation functions
def connect_to_workers(n_workers):
    return [
        sy.VirtualWorker(hook, id=f"worker{i+1}")
        for i in range(n_workers)
    ]
def connect_to_crypto_provider():
    return sy.VirtualWorker(hook, id="crypto_provider")

workers = connect_to_workers(n_workers=2)
crypto_provider = connect_to_crypto_provider()

获取访问权限和秘密共享数据

在这里,我们使用一个效用函数来模拟以下行为:我们假设MNIST数据集分布在各个部分中,每个部分都由我们的一个工作机持有。 然后,工作机将其数据分批拆分,并在彼此之间秘密共享其数据。 返回的最终对象是这些秘密共享批次上的可迭代对象,我们称之为“私有数据加载器”。 请注意,在此过程中,本地工作人员(因此我们)从未访问过数据。

我们像往常一样获得了训练和测试私有数据集,并且输入和标签都是秘密共享的。


In [ ]:
def get_private_data_loaders(precision_fractional, workers, crypto_provider):
    
    def one_hot_of(index_tensor):
        """
        Transform to one hot tensor
        
        Example:
            [0, 3, 9]
            =>
            [[1., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
             [0., 0., 0., 1., 0., 0., 0., 0., 0., 0.],
             [0., 0., 0., 0., 0., 0., 0., 0., 0., 1.]]
            
        """
        onehot_tensor = torch.zeros(*index_tensor.shape, 10) # 10 classes for MNIST
        onehot_tensor = onehot_tensor.scatter(1, index_tensor.view(-1, 1), 1)
        return onehot_tensor
        
    def secret_share(tensor):
        """
        Transform to fixed precision and secret share a tensor
        """
        return (
            tensor
            .fix_precision(precision_fractional=precision_fractional)
            .share(*workers, crypto_provider=crypto_provider, requires_grad=True)
        )
    
    transformation = transforms.Compose([
        transforms.ToTensor(),
        transforms.Normalize((0.1307,), (0.3081,))
    ])
    
    train_loader = torch.utils.data.DataLoader(
        datasets.MNIST('../data', train=True, download=True, transform=transformation),
        batch_size=args.batch_size
    )
    
    private_train_loader = [
        (secret_share(data), secret_share(one_hot_of(target)))
        for i, (data, target) in enumerate(train_loader)
        if i < n_train_items / args.batch_size
    ]
    
    test_loader = torch.utils.data.DataLoader(
        datasets.MNIST('../data', train=False, download=True, transform=transformation),
        batch_size=args.test_batch_size
    )
    
    private_test_loader = [
        (secret_share(data), secret_share(target.float()))
        for i, (data, target) in enumerate(test_loader)
        if i < n_test_items / args.test_batch_size
    ]
    
    return private_train_loader, private_test_loader
    
    
private_train_loader, private_test_loader = get_private_data_loaders(
    precision_fractional=args.precision_fractional,
    workers=workers,
    crypto_provider=crypto_provider
)

模型规格

这是我们将使用的模型,它是一个相当简单的模型,但是已证明在MNIST上表现良好


In [ ]:
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.fc1 = nn.Linear(28 * 28, 128)
        self.fc2 = nn.Linear(128, 64)
        self.fc3 = nn.Linear(64, 10)

    def forward(self, x):
        x = x.view(-1, 28 * 28)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x

训练和测试功能

训练几乎像往常一样进行,真正的区别是我们不能使用像负对数可能性(PyTorch中的F.nll_loss)之类的损失,因为使用SMPC再现这些功能相当复杂。相反,我们使用更简单的均方误差损失。


In [ ]:
def train(args, model, private_train_loader, optimizer, epoch):
    model.train()
    for batch_idx, (data, target) in enumerate(private_train_loader): # <-- now it is a private dataset
        start_time = time.time()
        
        optimizer.zero_grad()
        
        output = model(data)
        
        # loss = F.nll_loss(output, target)  <-- not possible here
        batch_size = output.shape[0]
        loss = ((output - target)**2).sum().refresh()/batch_size
        
        loss.backward()
        
        optimizer.step()

        if batch_idx % args.log_interval == 0:
            loss = loss.get().float_precision()
            print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}\tTime: {:.3f}s'.format(
                epoch, batch_idx * args.batch_size, len(private_train_loader) * args.batch_size,
                100. * batch_idx / len(private_train_loader), loss.item(), time.time() - start_time))

测试功能不变!


In [ ]:
def test(args, model, private_test_loader):
    model.eval()
    test_loss = 0
    correct = 0
    with torch.no_grad():
        for data, target in private_test_loader:
            start_time = time.time()
            
            output = model(data)
            pred = output.argmax(dim=1)
            correct += pred.eq(target.view_as(pred)).sum()

    correct = correct.get().float_precision()
    print('\nTest set: Accuracy: {}/{} ({:.0f}%)\n'.format(
        correct.item(), len(private_test_loader)* args.test_batch_size,
        100. * correct.item() / (len(private_test_loader) * args.test_batch_size)))

让我们开始训练吧!

关于这里发生的事情的一些注意事项。首先,我们秘密共享所有工作机的所有模型参数。其次,我们将优化程序的超参数转换为固定精度。注意,我们不需要秘密共享它们,因为它们在我们的上下文中是公共的,但是当秘密共享值存在于有限域中时,我们仍然需要使用.fix_precision将它们移入有限域中,以便执行一致的操作像权重更新: $W \leftarrow W - \alpha * \Delta W$.


In [ ]:
model = Net()
model = model.fix_precision().share(*workers, crypto_provider=crypto_provider, requires_grad=True)

optimizer = optim.SGD(model.parameters(), lr=args.lr)
optimizer = optimizer.fix_precision() 

for epoch in range(1, args.epochs + 1):
    train(args, model, private_train_loader, optimizer, epoch)
    test(args, model, private_test_loader)

在这!使用MNIST数据集的一小部分,使用100%加密的训练,您只能获得75%的准确性!

2. 讨论区

通过分析我们刚刚做的事情,让我们更仔细地了解加密训练的功能。

2.1 计算时间

第一件事显然是运行时间!您肯定已经注意到,它比明文训练要慢得多。特别是,在1批64项上进行一次迭代需要3.2s,而在纯PyTorch中只有13 ms。尽管这似乎是一个阻塞程序,但请回想一下,这里的所有事情都是远程发生的,并且是在加密的世界中发生的:没有单个数据项被公开。更具体地说,处理一项的时间是50ms,这还不错。真正的问题是分析何时需要加密训练以及何时仅加密预测就足够了。例如,在生产就绪的情况下,完全可以接受50毫秒执行预测!

一个主要的瓶颈是昂贵的激活功能的使用:SMPC的relu激活非常昂贵,因为它使用私有比较和SecureNN协议。例如,如果我们用二次激活代替relu,就像在CryptoNets等加密计算的几篇论文中所做的那样,我们将从3.2s降到1.2s。

通常,关键思想是仅加密必需的内容,本教程向您展示它可以多么简单。

2.2 SMPC的反向传播

您可能会想知道,尽管我们在有限域中使用整数,但我们如何执行反向传播和梯度更新。为此,我们开发了一个新的syft张量,称为AutogradTensor。 尽管您可能没有看过本教程,但它还是大量使用它! 让我们通过打印模型的重量进行检查:


In [ ]:
model.fc3.bias

和一个数据项:


In [ ]:
first_batch, input_data = 0, 0
private_train_loader[first_batch][input_data]

正如您所看到的,AutogradTensor在那里! 它位于Torch包装器和FixedPrecisionTensor之间,这表明值现在位于有限域中。此AutogradTensor的目标是在对加密值进行操作时存储计算图。这很有用,因为在向后调用反向传播时,此AutogradTensor会覆盖所有与加密计算不兼容的向后函数,并指示如何计算这些梯度。 例如,对于使用Beaver三元组技巧完成的乘法,我们不想再对技巧进行区分,因为区分乘法应该非常容易:$\partial_b(a\cdot b)= \cdot \partial b$。 例如,这是我们描述如何计算这些梯度的方法:

class MulBackward(GradFunc):
    def __init__(self, self_, other):
        super().__init__(self, self_, other)
        self.self_ = self_
        self.other = other

    def gradient(self, grad):
        grad_self_ = grad * self.other
        grad_other = grad * self.self_ if type(self.self_) == type(self.other) else None
        return (grad_self_, grad_other)

如果您想知道更多我们如何实现梯度的,可以查看tensors / interpreters / gradients.py

就计算图而言,这意味着该图的副本保留在本地,并且协调正向传递的服务器还提供有关如何进行反向传递的指令。 在我们的环境中,这是一个完全正确的假设。

2.3 安全保障

最后,让我们给出一些有关我们在此处实现的安全性的提示:我们正在考虑的对手诚实但好奇:这意味着对手无法通过运行此协议来了解有关数据的任何信息,但是恶意的对手仍可能偏离协议,例如尝试破坏共享以破坏计算。在这样的SMPC计算(包括私有比较)中针对恶意对手的安全性仍然是一个未解决的问题。

此外,即使安全多方计算确保不访问训练数据,此处仍然存在来自纯文本世界的许多威胁。例如,当您可以向模型提出请求时(在MLaaS的上下文中),您可以获得可能泄露有关训练数据集信息的预测。特别是,您没有针对成员资格攻击的任何保护措施,这是对机器学习服务的常见攻击,在这种攻击中,对手想确定数据集中是否使用了特定项目。除此之外,其他攻击,例如意外的记忆过程(学习有关数据项特定特征的模型),模型反演或提取仍然可能。

对上述许多威胁有效的一种通用解决方案是添加差异隐私。它可以与安全的多方计算完美地结合在一起,并且可以提供非常有趣的安全性保证。我们目前正在研究几种实现方式,并希望提出一个将两者结合起来的示例!

恭喜!!! 是时候加入社区了!

祝贺您完成本笔记本教程! 如果您喜欢此方法,并希望加入保护隐私、去中心化AI和AI供应链(数据)所有权的运动,则可以通过以下方式做到这一点!

给 PySyft 加星

帮助我们的社区的最简单方法是仅通过给GitHub存储库加注星标! 这有助于提高人们对我们正在构建的出色工具的认识。

选择我们的教程

我们编写了非常不错的教程,以更好地了解联合学习和隐私保护学习的外观,以及我们如何为实现这一目标添砖加瓦。

加入我们的 Slack!

保持最新进展的最佳方法是加入我们的社区! 您可以通过填写以下表格来做到这一点http://slack.openmined.org

加入代码项目!

对我们的社区做出贡献的最好方法是成为代码贡献者! 您随时可以转到PySyft GitHub的Issue页面并过滤“projects”。这将向您显示所有概述,选择您可以加入的项目!如果您不想加入项目,但是想做一些编码,则还可以通过搜索标记为“good first issue”的GitHub问题来寻找更多的“一次性”微型项目。

捐赠

如果您没有时间为我们的代码库做贡献,但仍想提供支持,那么您也可以成为Open Collective的支持者。所有捐款都将用于我们的网络托管和其他社区支出,例如黑客马拉松和聚会!

OpenMined's Open Collective Page


In [ ]:


In [ ]: